package ua.cn.store.impl;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceException;

import org.springframework.orm.jpa.JpaCallback;
import org.springframework.orm.jpa.JpaTemplate;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import ua.cn.store.domain.Domain;
import ua.cn.store.service.IGenericService;
import ua.cn.store.service.GeneralServiceException;

public abstract class GenericServiceImpl<T extends Domain> implements IGenericService<T>{

	private static final String QUERY_SELECT_ALL = "SELECT x FROM %s x"; //$NON-NLS-1$
		
	/** Spring's jpa template that is used to access db */
	protected JpaTemplate jpaTemplate;
	
	/** Persistent class that this dao works with */
	protected Class<T> persistentClass;

	/** Log object used for logging */
	protected Log log = LogFactory.getLog(getClass());

	/**
	 * Constructor with fields
	 * 
	 * @param persistentClass
	 *            Class that this dao will work with
	 */
	public GenericServiceImpl(Class<T> persistentClass) {
		super();
		this.persistentClass = persistentClass;
	}
	
	/**
	 * @return the jpaTemplate
	 */
	public JpaTemplate getJpaTemplate() {
		return jpaTemplate;
	}

	/**
	 * @param jpaTemplate the jpaTemplate to set
	 */
	public void setJpaTemplate(JpaTemplate jpaTemplate) {
		this.jpaTemplate = jpaTemplate;
	}
	
	/**
	 * <p>
	 * This template method executes query with performing all needed
	 * operations, like creating EntityManager, creating transaction,
	 * committing, or rolling it back.
	 * </p>
	 * <p>
	 * Methods sets parameters for the query as they appear in the parameters
	 * list, by number starting from 1. So, the first parameter in your named
	 * query should be referenced as <code>?1</code>, second as <code>?2</code>
	 * and so on.
	 * </p>
	 * <p>
	 * If <code>singleResult = true</code> and no result is found, then
	 * <code>null</code> is returned.
	 * </p>
	 * <p>
	 * Be aware, that when multiple results are returned, they are being
	 * dynamically casted to <code>REZ</code> class. It should be able to cast
	 * to {@link java.util.List}. When returning single result, <code>REZ</code>
	 * should be a single persistent entity class.
	 * </p>
	 * 
	 * @param <REZ>
	 *            Class of the result
	 * @param queryOrQueryName
	 *            Query string or NamedQuery name
	 * @param namedQuery
	 *            Specifies, whether query is named query
	 * @param singleResult
	 *            Specifies, whether single result should be returned
	 * @param initializeObjects
	 *            If true, then function will iterate through all object to
	 *            initialize them
	 * @param parameters
	 *            Parameters. You can specify multiple parameters separated by
	 *            comma
	 * @return Result of the query
	 * @throws PersistenceException
	 *             If error occurs
	 */
	@SuppressWarnings("unchecked")
	@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
	protected <REZ> REZ executeQuery(String queryOrQueryName,
			boolean namedQuery, boolean singleResult, Object... parameters) {
		
		if (StringUtils.isBlank(queryOrQueryName)) {
			throw new IllegalArgumentException(
					"Query for executing cannot be null");
		}

		if (log.isDebugEnabled()) {
			log.debug(String.format(
				"Executing query '%s' to return single result '%s' with params %s", //$NON-NLS-1$
					queryOrQueryName, singleResult, ArrayUtils
						.toString(parameters)));
		}

		REZ result;
		List<?> list;

		// Executing either named or simple query
		if (namedQuery) {
			list = jpaTemplate.findByNamedQuery(queryOrQueryName, parameters);
		} else {
			list = jpaTemplate.find(queryOrQueryName, parameters);
		}

		// Returning needed result
		if (singleResult) {
			if (CollectionUtils.isNotEmpty(list)) {
				result = (REZ) list.get(0);
			} else {
				result = null;
			}
		} else {
			result = (REZ) list;
		}

		return result;
	}
	
	@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
	protected int executeUpdateQuery(final String queryOrQueryName,
			final boolean namedQuery, final Object... parameters) 
			throws IllegalArgumentException, GeneralServiceException{
		if (StringUtils.isBlank(queryOrQueryName)) {
			throw new IllegalArgumentException(
					"Query for executing cannot be null");
		}

		if (log.isDebugEnabled()) {
			log.debug(String.format(
				"Executing update query '%s' to return single result '%s' with params %s", //$NON-NLS-1$
					queryOrQueryName, ArrayUtils
						.toString(parameters)));
		}
		try{
		Object rez = jpaTemplate.execute(new JpaCallback() {
			
			@Override
			public Object doInJpa(EntityManager em) throws PersistenceException {
				javax.persistence.Query query;
				if (namedQuery){
					query = em.createNamedQuery(queryOrQueryName);
				}else{
					query = em.createQuery(queryOrQueryName);
				}
					if (parameters.length > 0){
						for (int i=0; i<parameters.length; i++){
							query.setParameter(i+1, parameters[i]);
						}
					}
				return Integer.valueOf(query.executeUpdate());
			}
		});
		return ((Integer)rez).intValue();
		}catch (Exception e){
			throw new GeneralServiceException(e);
		}
	}	
	
	/**
	 * Method that is called, before entity is being updated or added. Method
	 * must be overwritten in subclasses to make some checks.
	 * 
	 * @param entity
	 *            Entity that will be added or updated
	 * @throws GeneralServiceException
	 *             If error occurs
	 */
	protected abstract void beforeEntityAddUpdate(T obj)
			throws GeneralServiceException;

	/**
	 * Method that is called just before entity is deleted
	 * @param entity Entity to delete
	 * @throws GeneralServiceException If error occurs
	 */
	protected abstract void beforeEntityDelete(T obj) throws GeneralServiceException;

	
	/* (non-Javadoc)
	 * @see ua.cn.yet.birthdayGifts.services.IGenericService#save(ua.cn.yet.birthdayGifts.domain.DomainObject)
	 */
	@Override
	@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
	public T add (T obj) throws IllegalArgumentException, GeneralServiceException {
		if (obj == null) {
			throw new IllegalArgumentException(
					"Object for saving cannot be null!");
		}

		T addedObj = null;

		try {

			if (obj.getId()== 0) {

				if (log.isDebugEnabled()) {
					log.debug("Saving new entity: " + obj); //$NON-NLS-1$
				}

				jpaTemplate.persist(obj);
				addedObj = obj;
			} else {

				if (log.isDebugEnabled()) {
					log.debug("Updating entity: " + obj); //$NON-NLS-1$
				}

				addedObj = jpaTemplate.merge(obj);
			}

		} catch (Exception e) {
			if (obj.getId() == 0) {
				throw new GeneralServiceException("GenericServiceImpl.ErrorFailedToAdd"); 
			} else {
				throw new GeneralServiceException("GenericServiceImpl.ErrorFailedToUpdate");
			}
		}

		return addedObj;
	}

	@Transactional(propagation = Propagation.REQUIRED, readOnly = false)
	public void delete(T obj) throws IllegalArgumentException, GeneralServiceException {
		if (obj == null) {
			throw new IllegalArgumentException("GenericServiceImpl.ErrorEntityNull"); //$NON-NLS-1$
		}

		if (obj.getId() == 0) {
			log.warn("Trying to delete transient entity: " + obj); //$NON-NLS-1$
			return;
		}
		
		beforeEntityDelete(obj);

		try {
			// Deleting entity object
			if (!jpaTemplate.contains(obj)) {
				if (log.isDebugEnabled()) {
					log.debug("Attaching detached entity before deleting " //$NON-NLS-1$
							+ obj);
				}
				T attachedEntity = jpaTemplate.merge(obj);
				jpaTemplate.remove(attachedEntity);
			} else {
				jpaTemplate.remove(obj);
			}
		} catch (Exception e) {
			// Catching all runtime exceptions that might occur when operating
			// with entity. Returning our service exception in such case
			throw new GeneralServiceException("GenericServiceImpl.ErrorFailToDelete"); //$NON-NLS-1$
					
		}
	}
	
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see biz.smartymedia.common.service.IGenericServiceNew#getAllEntites()
	 */
	@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
	public List<T> getAll() {
		try {
			// Getting all 
			List<T> list = executeQuery(String.format(QUERY_SELECT_ALL, persistentClass
					.getSimpleName()), false, false);
			if (list != null)
				return list;
			else
				return new ArrayList<T>();

		} catch (Exception e) {
			log.error("Failed to get list of all entities", e); 
			return new ArrayList<T>();
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * biz.smartymedia.common.service.IGenericServiceNew#getEntityById(java.
	 * lang.Long)
	 */
	@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)
	public T findById(Long id) throws IllegalArgumentException {
		if (id == null) {
			throw new IllegalArgumentException("GenericDaoJpa.ErrorEntityIdNull"); 
		}
		if (log.isDebugEnabled()) {
			log.debug("Getting entity " + persistentClass.getSimpleName() //$NON-NLS-1$
					+ " by id " + id); //$NON-NLS-1$
		}
		try {
			// Getting by id
			T addedobj = jpaTemplate.find(persistentClass, id);

			// Checking if we got the entity that is not NULL
			if ((addedobj == null) || (addedobj.getId() == 0)) {
				StringBuilder mess = new StringBuilder("Entity by id "); //$NON-NLS-1$
				
				mess.append(id).append(" was not found"); //$NON-NLS-1$
				
				log.warn(mess.toString());
				
				return null;
			}

			return addedobj;
		} catch (Exception e) {
			log.error("Failed to get entity by id " + id, e); //$NON-NLS-1$
			return null;
		}
	}
	
	
	
	
}
